﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using GraphosData;
using System.Windows;
using System.Windows.Media;
using System.Windows.Threading;
using System.Windows.Media.Animation;
using System.ComponentModel;
using System.Windows.Controls;

namespace GraphosControls
{

    public class ZoomRequestedArgs
    {
        public ZoomRequestedArgs(double sug)
        {
            SuggestedValue = sug;
        }
        public double SuggestedValue { get; set; }
    }

    /// <summary>
    /// This class is in charge of handling the main animation in GraphViewer
    /// </summary>
    public class PositionUtility : INotifyPropertyChanged
    {
        public PositionUtility()
        {
            // TODO : These defaults could be centralized in the app somewhere possibly
            TimeStep = 0.015;
            Spring = 120;
            Damping = 0.90;
            EdgeLength = 100;
            Charge = 1200;

            MaxVelocity = 800;
            SizeModifier = 1.0;
            Size = Size.Empty;
        }

        #region Properties

        public double TimeStep
        {
            get { return m_timeStep; }
            set { m_timeStep = value; }
        }
        public double Charge
        {
            get { return m_charge; }
            set
            {
                m_charge = value;
                OnPropertyChanged("Charge");
                Restart();
            }
        }
        public double MaxVelocity
        {
            get { return m_maxVelocity; }
            set
            {
                m_maxVelocity = value;
                OnPropertyChanged("MaxVelocity");
                Restart();
            }
        }
        public double EdgeLength
        {
            get { return m_edgeLength; }
            set
            {
                m_edgeLength = value;
                OnPropertyChanged("EdgeLength");
                Restart();
            }
        }
        public double RootEdgeLength
        {
            get { return m_rootEdgeLength; }
            set
            {
                m_rootEdgeLength = value;
                OnPropertyChanged("RootEdgeLength");
                Restart();
            }
        }
        public double Spring
        {
            get { return m_spring; }
            set
            {
                m_spring = value;
                OnPropertyChanged("Spring");
                Restart();
            }
        }
        public double Damping
        {
            get { return m_damping; }
            set
            {
                m_damping = value;
                OnPropertyChanged("Damping");
                Restart();
            }
        }

        public VisualNode RootNode
        {
            get { return m_rootNode; }
            set
            {
                if (m_rootNode != value)
                {
                    m_rootNode = value;
                }
            }
        }
        public List<VisualEdge> Edges
        {
            get { return m_edges; }
            set
            {
                m_edges = value;
                KineticEnergyCutOff = Math.Pow(Edges.Count, 1.5);
            }
        }
        public Size Size
        {
            get { return m_size; }
            set
            {
                m_size = value;
                if (m_size != Size.Empty)
                {
                    CenterSpringPosition = new Point(Size.Width / 2, Size.Height / 2);

                    try
                    {
                        m_edgeLength = Math.Max(100, Math.Min(200, Math.Sqrt(Math.Pow(Size.Width, 2) + Math.Pow(Size.Height, 2)) / ((Edges.Count + RootNode.AdjacentVisualNodes.Count) / 4)));
                        m_rootEdgeLength = Math.Max(2 * (Edges.Count / RootNode.AdjacentVisualNodes.Count), Math.Min(300, Math.Sqrt(Math.Pow(Size.Width, 2) + Math.Pow(Size.Height, 2)) / (Edges.Count / 4)));
                    }
                    catch (Exception)
                    {
                        
                        m_edgeLength = 200;
                        m_rootEdgeLength = 300;
                    }
                    

                    
                    }
            }
        }
        public double KineticEnergyCutOff { get; set; }
        public bool IsReadyToAnimate
        {
            get { return Size != Size.Empty && RootNode != null && Edges != null; }
        }

        public VisualNode DragNode
        {
            get { return m_dragNode; }
            set
            {
                if (m_dragNode != value)
                {
                    if (value == null)
                    {
                        ReleaseDragNode();
                        return;
                    }

                    if (m_dragNode != null)
                        ReleaseDragNode();

                    SetDragNode(value);
                }
            }
        }
        public Point DragPosition
        {
            get { return m_dragPosition; }
            set
            {
                m_dragPosition = value;
                Restart();
            }
        }
        public bool Dragging { get; private set; }

        public bool IsAnimating { get; private set; }
        public Point CenterSpringPosition { get; set; }
        public double SizeModifier { get; private set; }
        public bool AllowAutoZoom { get; set; }
        public double UpperBumperThreshold { get; set; }
        public double LowerBumperThreshold { get; set; }

        #endregion

        #region Public Methods

        public void Start(VisualNode rootNode, List<VisualEdge> edges, double width, double height)
        {
            RootNode = rootNode;
            Edges = edges;
            Size = new Size(width, height);

            UpperBumperThreshold = 1.00;
            LowerBumperThreshold = 0.45;

            Restart();
        }
        public void Restart()
        {
            if (!IsReadyToAnimate)
                return;

            if (!IsAnimating)
            {
                // add the handler for the animation
                CompositionTarget.Rendering += PositionAnimation_Rendering;
                IsAnimating = true;
            }

            // reset the zoom counter
            m_autoZoomCounter = 0;

            // set all edge charges
            foreach (VisualEdge edge in Edges)
                edge.CenterNode.Charge = Charge;


            // set all the node charges
            foreach (var node in RootNode.AdjacentVisualNodes)
            {
                node.Charge = Charge;
            }
        }
        public void Pause()
        {
            // NOTE: this method is not currently used, but i'm not convinced yet that 
            //       it should be removed.
            CompositionTarget.Rendering -= PositionAnimation_Rendering;
            OnPositionAnimationComplete();
            IsAnimating = false;
        }
        public void Stop()
        {

            // signal auto zoom if need be
            if (AllowAutoZoom)
                OnZoomRequested(SizeModifier);

            // remove event handler for animation
            CompositionTarget.Rendering -= PositionAnimation_Rendering;

            // reset all nodes and edges
            foreach (var node in RootNode.AdjacentVisualNodes)
            {
                node.ResetKeneticValues();
            }
            foreach (VisualEdge edge in Edges)
            {
                edge.CenterNode.ResetKeneticValues();
            }

            // let everyone know that you are done
            OnPositionAnimationComplete();
            IsAnimating = false;

        }

        #endregion

        #region Frame Logic

        private void PositionAnimation_Rendering(object sender, EventArgs e)
        {
            // ////////////////////////////////////////////////////
            // TODO: This code could use some comments from Michael
            // ////////////////////////////////////////////////////

            double totalKineticEnergy = 0;
            double averageBumperForce = 0.0;

            if (Dragging)
            {
                // set the drag point if there is a node being drug
                DragNode.NetForce = CalculateAttraction(DragNode.Position, DragPosition, 0, 100);
                DragNode.Velocity = (RootNode.Velocity + TimeStep * DragNode.NetForce) * Damping;
                DragNode.Position += TimeStep * DragNode.Velocity;
                totalKineticEnergy += DragNode.Velocity.LengthSquared;
            }
            else
            {
                // otherwise set the center point
                RootNode.NetForce = CalculateAttraction(RootNode.Position, CenterSpringPosition, 0, Spring);
                RootNode.Velocity = GetVelocity((RootNode.Velocity + TimeStep * RootNode.NetForce) * Damping);
                RootNode.Position += TimeStep * RootNode.Velocity;
                totalKineticEnergy += RootNode.Velocity.LengthSquared;
            }

            foreach (VisualEdge edge in Edges)
            {
                edge.CenterNode.NetForce = new Vector(0, 0);

                foreach (VisualEdge otherEdge in Edges.Where(ed => ed != edge))
                    edge.CenterNode.NetForce += CalculateRepulsion(edge.CenterNode.Position, otherEdge.CenterNode.Position, 40, 40);

                edge.CenterNode.NetForce += CalculateAttraction(edge.CenterNode.Position, edge.VisualNodeFrom.Position, edge.Length / 2.0, 40);
                edge.CenterNode.NetForce += CalculateAttraction(edge.CenterNode.Position, edge.VisualNodeTo.Position, edge.Length / 2.0, 40);

                if (edge.VisualNodeFrom != RootNode && edge.VisualNodeTo != RootNode)
                {
                    edge.CenterNode.NetForce += CalculateRepulsion(edge.CenterNode.Position, RootNode.Position, Charge / 6, Charge / 6);
                }
                if (edge.VisualNodeFrom != RootNode)
                    edge.VisualNodeFrom.NetForce += CalculateAttraction(edge.VisualNodeFrom.Position, edge.CenterNode.Position, edge.Length / 2, Spring / 2);
                if (edge.VisualNodeTo != RootNode)
                    edge.VisualNodeTo.NetForce += CalculateAttraction(edge.VisualNodeTo.Position, edge.CenterNode.Position, edge.Length / 2, Spring / 2);
                edge.CenterNode.Velocity = (edge.CenterNode.Velocity + TimeStep * edge.CenterNode.NetForce) * Damping;
                edge.CenterNode.Position += TimeStep * edge.CenterNode.Velocity;
            }


            // ////////////////////////////////////////////////////////////////////
            // TODO : need to find a nice way to stop the animation at a good point
            //        for small number of nodes and large (1-40 or so), it can
            //        circle around which is kind of annoying
            // ////////////////////////////////////////////////////////////////////

            foreach (VisualNode node in RootNode.AdjacentVisualNodes.Where(n => n != DragNode))
            {
                foreach (VisualNode otherNode in AllVisualNodes(RootNode).Where(n => n != node))
                {
                    node.NetForce += CalculateRepulsion(node.Position, otherNode.Position, node.Charge, otherNode.Charge);
                }
                foreach (VisualNode connectedNode in node.AdjacentVisualNodes)
                {
                    if (connectedNode == RootNode)
                    {
                        double s = node.AdjacentVisualNodes.Count == 1 ? Spring * 2 : Spring;
                        node.NetForce += CalculateAttraction(node.Position, connectedNode.Position, RootEdgeLength, s);
                    }
                    else
                    {
                        node.NetForce += CalculateAttraction(node.Position, connectedNode.Position, EdgeLength, Spring);
                    }
                }

                double xdiff = ((Size.Width / SizeModifier) - Size.Width) / 2;
                double ydiff = ((Size.Height / SizeModifier) - Size.Height) / 2;

                var bumpers = new Point[] {
                    new Point(node.Position.X, (node.Position.Y - Math.Abs(node.Position.Y + ydiff))),
                    new Point(node.Position.X, (node.Position.Y + Math.Abs(Size.Height - node.Position.Y + ydiff))),
                    new Point((node.Position.X - Math.Abs(node.Position.X + xdiff)), node.Position.Y),
                    new Point((node.Position.X + Math.Abs(Size.Width - node.Position.X + xdiff)), node.Position.Y)};

                if (AllowAutoZoom)
                {
                    double maxForce = 0.0;
                    foreach (var bumper in bumpers)
                    {
                        Vector bumperForce = CalculateRepulsion(node.Position, bumper, node.Charge, node.Charge);
                        if (bumperForce.Length > maxForce)
                            maxForce = bumperForce.Length;
                        node.NetForce += bumperForce;
                    }
                    averageBumperForce += maxForce;
                }
                else
                {
                    foreach (var bumper in bumpers)
                    {
                        node.NetForce += CalculateRepulsion(node.Position, bumper, node.Charge, node.Charge);
                    }
                }

                node.Velocity = GetVelocity((node.Velocity + TimeStep * node.NetForce) * Damping);
                node.Position += TimeStep * node.Velocity;
                totalKineticEnergy += node.Velocity.LengthSquared;
                node.NetForce = new Vector(0, 0);
            }

            if (AllowAutoZoom)
            {
                averageBumperForce /= RootNode.AdjacentVisualNodes.Count;
                double bumperChargeModified = 1000 * averageBumperForce / (EdgeLength * Spring * Charge);

                // increase or decrease the bumper distances
                if (bumperChargeModified > UpperBumperThreshold)
                    SizeModifier -= 0.05;
                else if (bumperChargeModified < LowerBumperThreshold)
                    SizeModifier += 0.05;

                m_autoZoomCounter++;

                // signal for auto zoom when the counter hits a certain value 
                if (m_autoZoomCounter >= 50)
                {
                    OnZoomRequested(SizeModifier);
                    m_autoZoomCounter = 0;
                }
            }

            // stop animating when total kinetic energy is small enough
            if (totalKineticEnergy < KineticEnergyCutOff)
            {
                Stop();
            }
        }


        #endregion

        #region Private Helpers

        private Vector GetVelocity(Vector v)
        {
            if (v.Length > MaxVelocity)
                return v / (v.Length / MaxVelocity);
            else if (v.Length < 10)
                return v / (10 / v.Length);
            return v;
        }

        private void SetDragNode(VisualNode node)
        {
            m_dragNode = node;
            Dragging = m_dragNode.CaptureMouse();

            if (!Dragging)
                ReleaseDragNode();
        }
        private void ReleaseDragNode()
        {
            m_dragNode.ReleaseMouseCapture();
            m_dragNode = null;
            DragPosition = new Point();
            Dragging = false;
        }

        // Get all the visual nodes (including the root)
        private static IEnumerable<VisualNode> AllVisualNodes(VisualNode root)
        {
            yield return root;
            foreach (VisualNode node in root.AdjacentVisualNodes)
                yield return node;
        }

        // Calculate the attraction of two visual nodes (or dummy nodes)
        private static Vector CalculateAttraction(Point sourcePosition, Point targetPosition,
             double edgeLength, double spring)
        {
            Vector force = (sourcePosition - targetPosition);
            if (force.Length == 0)
                return new Vector(0, 0);
            double displacement = force.Length - edgeLength;

            force.Normalize();
            force.Negate();

            return spring * force * displacement;
        }

        // Calculate the repulsion of two visual nodes (or dummy nodes)
        private static Vector CalculateRepulsion(Point sourcePosition, Point targetPosition,
             double sourceCharge, double targetCharge)
        {
            Vector force = (sourcePosition - targetPosition);
            if (force.Length == 0)
                return new Vector(0, 0);

            return (sourceCharge * targetCharge * force) / force.LengthSquared;
        }

        #endregion

        #region Events

        public event RoutedEventHandler PositionAnimationComplete;
        private void OnPositionAnimationComplete()
        {
            if (PositionAnimationComplete != null)
                PositionAnimationComplete(this, new RoutedEventArgs());
        }

        public delegate void ZoomRequestedHandler(object sender, ZoomRequestedArgs e);
        public event ZoomRequestedHandler ZoomRequested;
        private void OnZoomRequested(double suggested)
        {
            if (ZoomRequested != null)
                ZoomRequested(this, new ZoomRequestedArgs(suggested));
        }

        #endregion

        #region Fields

        private double m_timeStep;
        private double m_charge;
        private double m_spring;
        private double m_edgeLength;
        private double m_rootEdgeLength;
        private double m_damping;
        private double m_maxVelocity;

        private VisualNode m_rootNode;

        private VisualNode m_dragNode;
        private Point m_dragPosition;

        private int m_autoZoomCounter;
        List<VisualEdge> m_edges;
        private Size m_size;

        #endregion

        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged(string info)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(info));
        }

        #endregion
    }
}
